package storage.data;


import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;
import java.util.regex.Pattern;

import exceptions.DBAddEntryException;
import exceptions.DBAllocateException;
import exceptions.DBBufferFull;
import exceptions.DBCannotDelPage;
import exceptions.DBCannotUnpin;
import exceptions.DBDelEntryException;
import exceptions.DBDiskMapException;
import exceptions.DBEntryNotExistException;
import exceptions.DBFullException;
import exceptions.DBGetEntryException;
import exceptions.DBInvalidPageIDException;
import exceptions.DBNoEntryToDelException;
import exceptions.DBNotInitException;
import exceptions.DBReadPageException;
import exceptions.DBUnpinPageNotExists;
import exceptions.DBWritePageException;
import exceptions.DataBaseDupCreatedException;
import exceptions.DbIOException;

import Common.GlobalConst;
import Common.GoggleDB;
import Transaction.Transaction;


public class StorageInterface {
	private RandomAccessFile fp  ;
	String dbFileName = null ;
	int numPages = 0 ;   // the number of total pages
	int mapPages = 0 ;   // the number of total Map pages
	int emptyCount = 0 ;  // the pointer to the next possible empty page, for the purpes of accelerating when allocating pages
	int pageRead = 0 ; 
	int pageWrote = 0 ;
	
	
	public int getNumPages () {
		return numPages ;
	}
	
	public int getMapPages () {
		return mapPages ;
	}
	
	public int getReadTimes() {
		return pageRead ;
	}
		
	public int getWriteTimes() {
		return pageWrote ; 
	}
	
	public int createDB(Transaction tx, String dbName, int totalPages )throws Exception  {
		if ( totalPages < 2 ) totalPages = 2;  
		fp = new RandomAccessFile ( dbName, "rw") ;
		dbFileName = dbName ;
		PageID fstPageID = new PageID ( 0 ) ;
		Page aPage = new Page( false ) ;
		
		GoggleDB.bufferMgr.pinPage( fstPageID, aPage , true ) ;
		FirstPage fstPage = new FirstPage ( aPage ) ;
		numPages = totalPages ;
		mapPages = ( totalPages - 1 ) / GlobalConst.PAGESIZE / 8 + 1 ; 
		fstPage.writeNextPage( tx, fstPageID, Page.INVALID_PAGE ) ;
		fstPage.writeNumPage( tx, fstPageID, numPages ) ; 
		fstPage.setUsed( true ) ;
		fstPage.setDirty( true );
		PageID tmpID = new PageID () ;
		for ( int i = 1 ; i <= mapPages ; i ++ ) {
			tmpID.pageId = i ;
			GoggleDB.bufferMgr.pinPage( tmpID , aPage, true) ;
			GoggleDB.bufferMgr.unpinPage( tmpID , true) ;
		}		
		for ( int i = 0 ; i <= mapPages ; i ++ ) {
			tmpID.pageId = i ;
			setDiskMap ( tmpID, 1 ) ;
		}
 		GoggleDB.bufferMgr.unpinPage( fstPageID, true ) ;
		
		return 0;
	}
	
	public int openDB(String dbName) throws FileNotFoundException, DBReadPageException, DBWritePageException, DBBufferFull, DBInvalidPageIDException, DBUnpinPageNotExists, DBCannotUnpin{
		
		fp = new RandomAccessFile ( dbName, "rw") ;
		PageID fstPageID = new PageID ( 0 ) ; 
		Page aPage = new Page ( false ) ;
		
		GoggleDB.bufferMgr.pinPage( fstPageID , aPage , false ) ;
		FirstPage fstPage = new FirstPage ( aPage ) ; 
		numPages = fstPage.getNumDbPage() ;
		mapPages = ( numPages - 1 ) / GlobalConst.PAGESIZE / 8 + 1 ; 
		GoggleDB.bufferMgr.unpinPage( fstPageID, false ) ;
		return 0; 
	}
	
	public int closeDB() throws IOException {
		fp.close() ;
		return 0 ;
	}
	
	public int destoryDB() throws IOException {
		fp.close() ; 
		( new File ( dbFileName )).delete() ;
		return 0 ; 
	}
	
	public synchronized PageID add_db ( Transaction tx, String dbname ) throws DataBaseDupCreatedException, DBAddEntryException{
		PageID lastPageID = new PageID ( 0 ) ; 
		PageID currentPage = new PageID ( 0 ) 	;
		boolean found = true ; 
		try {
			get_db_by_name ( dbname ) ;   
		} catch ( Exception e ) {
			found = false ; 
		}
		if ( found ) throw new DataBaseDupCreatedException( null, dbname );
		 
		Page aPage = new Page ( false ) ;
		try {
			while ( currentPage.pageId != Page.INVALID_PAGE.pageId ) { 
				GoggleDB.bufferMgr.pinPage( currentPage , aPage, false );

				HeaderPage headerPage ;
				if ( currentPage.pageId == 0 ) headerPage = new FirstPage ( aPage );
				else headerPage = new HeaderPage ( aPage ) ;

				int i ; 
				for ( i = 0 ; i < headerPage.NUM_ENTRY ; i ++ ) {
					NameEntry entry = headerPage.getEntry( i ) ;
					if ( entry.firstPage.pageId == 0 ) break ; 
				}
				if ( i < headerPage.NUM_ENTRY ) {
					PageID newPageId = allocatePage () ;
					headerPage.writeEntry( tx, currentPage, i , new NameEntry ( dbname , newPageId ) ) ; 
					GoggleDB.bufferMgr.unpinPage( currentPage, true );
					return newPageId ;  
				}
				GoggleDB.bufferMgr.unpinPage( currentPage, false );
				
				lastPageID.pageId = currentPage.pageId ; 
				currentPage = headerPage.getNextPage() ; 
			}
			
			/*
			 * no empty file entry space in current pages.
			 * add new file entry page
			 */
			PageID newID = allocatePage () ;
			PageID dbPageID = allocatePage () ;
			HeaderPage newPage = new HeaderPage ( false ) ;
			HeaderPage lastPage = new HeaderPage ( false ) ;
			GoggleDB.bufferMgr.pinPage( lastPageID , lastPage, false ); 
			lastPage.writeNextPage( tx, lastPageID, newID ) ;
			GoggleDB.bufferMgr.unpinPage( lastPageID, true ) ;
			
			GoggleDB.bufferMgr.pinPage( newID, newPage, true) ;
			newPage.writeEntry( tx, newID, 0 , new NameEntry ( dbname, dbPageID )) ;
			newPage.writeNextPage( tx, newID, Page.INVALID_PAGE ) ;
			GoggleDB.bufferMgr.unpinPage( newID, true ) ; 
			return dbPageID ; 
		} catch ( Exception e ) {
			e.printStackTrace() ; 
			throw new DBAddEntryException ( e, dbname ) ;
		}
	}	
	
	public synchronized void del_db ( Transaction tx, String dbname ) throws DBDelEntryException, DBNoEntryToDelException {
		PageID tmpPage = new PageID ( 0 ) ; 
		PageID currentPage = new PageID ( 0 ) 	;
		
		try {
			while ( currentPage.pageId != Page.INVALID_PAGE.pageId ) { 
				Page aPage = new Page ( false ) ;
				GoggleDB.bufferMgr.pinPage( currentPage , aPage, false );

				HeaderPage headerPage ;
				if ( currentPage.pageId == 0 ) headerPage = new FirstPage ( aPage );
				else headerPage = new HeaderPage ( aPage ) ;

				int i ;
				NameEntry entry = null ;
				for ( i = 0 ; i < headerPage.NUM_ENTRY ; i ++ ) {
					entry = headerPage.getEntry( i ) ;
					if ( entry.firstPage.pageId == 0 ) continue ;
					else if ( entry.getName().equals( dbname )) break ; 
				}
				if ( i < headerPage.NUM_ENTRY ) {
					tmpPage.pageId = 0 ; 
					headerPage.writeEntry( tx, currentPage, i , new NameEntry ( dbname , tmpPage ) ) ;
					GoggleDB.bufferMgr.unpinPage( currentPage, true );
					GoggleDB.bufferMgr.deallocatePage( entry.getFirstPage() , 1) ;
					return ; 
				}
				tmpPage.pageId = currentPage.pageId ; 
				currentPage = headerPage.getNextPage() ; 
				GoggleDB.bufferMgr.unpinPage( tmpPage, false );				
			}		
		}
		catch ( Exception e ) {
			e.printStackTrace() ;
			throw new DBDelEntryException (e, dbname ) ; 
		}
		throw new DBNoEntryToDelException ( null , dbname ) ; 
	}
	
	public synchronized PageID get_db_by_name (String dbname ) throws DBGetEntryException, DBEntryNotExistException{
		PageID tmpPage = new PageID ( 0 ) ; 
		PageID currentPage = new PageID ( 0 ) 	;
		
		try {
			while ( currentPage.pageId != Page.INVALID_PAGE.pageId ) { 
				Page aPage = new Page ( false ) ;
				GoggleDB.bufferMgr.pinPage( currentPage , aPage, false );

				HeaderPage headerPage ;
				if ( currentPage.pageId == 0 ) headerPage = new FirstPage ( aPage );
				else headerPage = new HeaderPage ( aPage ) ;

				int i ; 
				for ( i = 0 ; i < headerPage.NUM_ENTRY ; i ++ ) {
					NameEntry entry = headerPage.getEntry( i ) ;
					if ( entry.firstPage.pageId == 0 ) continue ;
					else if ( entry.getName().equals( dbname )) {
							GoggleDB.bufferMgr.unpinPage( currentPage ,  false ) ; 
							return entry.firstPage ; 
						}
				}
				
				tmpPage.pageId = currentPage.pageId ; 
				currentPage = headerPage.getNextPage() ; 
				GoggleDB.bufferMgr.unpinPage( tmpPage, false );				
			}		
		}
		catch ( Exception e ) {
			throw new DBGetEntryException (e, dbname ) ; 
		}
		throw new DBEntryNotExistException ( null , dbname ) ; 
	}
	
	public synchronized ArrayList<String> get_tables_by_db (String dbname ) 
	throws DBGetEntryException, DBEntryNotExistException {
		PageID tmpPage = new PageID ( 0 ) ; 
		PageID currentPage = new PageID ( 0 ) 	;
		//dbname = dbname.toLowerCase() ;
		ArrayList < String > result = new ArrayList < String > () ;  
		
		try {
			while ( currentPage.pageId != Page.INVALID_PAGE.pageId ) { 
				Page aPage = new Page ( false ) ;
				GoggleDB.bufferMgr.pinPage( currentPage , aPage, false );

				HeaderPage headerPage ;
				if ( currentPage.pageId == 0 ) headerPage = new FirstPage ( aPage );
				else headerPage = new HeaderPage ( aPage ) ;

				int i ; 
				for ( i = 0 ; i < headerPage.NUM_ENTRY ; i ++ ) {
					NameEntry entry = headerPage.getEntry( i ) ;
					if ( entry.firstPage.pageId == 0 ) continue ;
					else if ( entry.getName().startsWith( dbname )) 
						result.add( entry.getName() ) ; 
				}
				
				tmpPage.pageId = currentPage.pageId ; 
				currentPage = headerPage.getNextPage() ; 
				GoggleDB.bufferMgr.unpinPage( tmpPage, false );				
			}
			return result ;
		}
		catch ( Exception e ) {
			throw new DBGetEntryException (e, dbname ) ; 
		}
	}
	
	public synchronized ArrayList<String> get_all_tables ( ) 
	throws DBGetEntryException, DBEntryNotExistException {
		PageID tmpPage = new PageID ( 0 ) ; 
		PageID currentPage = new PageID ( 0 ) 	;
		ArrayList < String > result = new ArrayList < String > () ;  
		
		try {
			while ( currentPage.pageId != Page.INVALID_PAGE.pageId ) { 
				Page aPage = new Page ( false ) ;
				GoggleDB.bufferMgr.pinPage( currentPage , aPage, false );

				HeaderPage headerPage ;
				if ( currentPage.pageId == 0 ) headerPage = new FirstPage ( aPage );
				else headerPage = new HeaderPage ( aPage ) ;

				int i ; 
				for ( i = 0 ; i < headerPage.NUM_ENTRY ; i ++ ) {
					NameEntry entry = headerPage.getEntry( i ) ;
					if ( entry.firstPage.pageId == 0 ) continue ;
					else result.add( entry.getName() ) ; 
				}
				
				tmpPage.pageId = currentPage.pageId ; 
				currentPage = headerPage.getNextPage() ; 
				GoggleDB.bufferMgr.unpinPage( tmpPage, false );				
			}
			return result ;
		}
		catch ( Exception e ) {
			throw new DBGetEntryException (e, "system" ) ; 
		}
	}
	
	
	private synchronized void setDiskMap ( PageID pageid , int bit ) throws DBDiskMapException {
		PageID mapPageID = new PageID ( pageid.pageId / ( 8 * GlobalConst.PAGESIZE )+ 1 ) ; 
		Page aPage = new Page ( false ); 
		try {
			GoggleDB.bufferMgr.pinPage(  mapPageID , aPage, false );
			MapPage mapPage = new MapPage( aPage ) ;
			mapPage.setPageBit( pageid , bit) ;
			GoggleDB.bufferMgr.unpinPage(  mapPageID, true );
		}
		catch ( Exception e ) {
			throw new DBDiskMapException ( e , pageid ) ;
		}
	}
	
	
	private synchronized int getDiskMap ( PageID pageid ) throws DBDiskMapException {
		int result ; 
		PageID mapPageID = new PageID ( pageid.pageId / ( 8 * GlobalConst.PAGESIZE ) + 1 ) ; 
		Page aPage = new Page ( false ); 
		try {
			GoggleDB.bufferMgr.pinPage(  mapPageID , aPage, false );
			MapPage mapPage = new MapPage( aPage ) ;
			result = mapPage.getPageBit( pageid ) ; 
			GoggleDB.bufferMgr.unpinPage(  mapPageID , false );
		}
		catch ( Exception e ) {
			throw new DBDiskMapException ( e , pageid ) ; 
		}
		return result ;  
	}
	

	public PageID allocatePage () throws DBAllocateException{
		return allocatePage ( 1 ) ; 
	}
	
	public synchronized PageID allocatePage ( int runs ) throws DBAllocateException{
		PageID pageid = new PageID ();
		try {
			int i ; 
			for ( i = 0 ; i < numPages - runs + 1 ; i ++  ) {
				int testPage =  ( i + emptyCount ) % numPages ;
				if ( testPage >= numPages - runs + 1 ) continue ; 
				int next ; 
				pageid.pageId =  testPage ;
				if ( (next = testRunsEmpty ( pageid, runs )) == runs ){
					emptyCount = testPage + runs ; 
					break ;
				}
				i += next ; 
			}
			if ( i < numPages - runs + 1 ){
				PageID tmp = new PageID () ; 
				for ( int j = 0 ; j < runs ; j ++ ) {
					tmp.pageId = ( pageid.pageId + j ) % numPages; 
					setDiskMap ( tmp , 1 ) ;
				}
//				String str = emptyCount + " " + pageid.pageId ;
	//			if ( emptyCount % 100 == 0 ) System.out.println( str ) ;
				return pageid ; 
			}
			else {
//				System.out.printf ( "i:%d pages:%d runs:%d\n", i , numPages, runs) ;
//				watchDiskMap (1 ) ;
				throw new DBFullException ( null ) ; 
			}
		}catch ( Exception e ) {
			e.printStackTrace() ;
			throw new DBAllocateException ( e ) ;
		}
	}	
	
	public void watchDiskMap ( int page ) {
		Page apage = new Page (); 
		PageID pid= new PageID ( page );
		try {
			GoggleDB.bufferMgr.pinPage( pid, apage, false );
			apage.showBits() ;
			GoggleDB.bufferMgr.unpinPage(pid, false) ;
		}
		catch (Exception e) {
			e.printStackTrace() ;
		}
	}
	
	int testRunsEmpty ( PageID pageid , int runs ) throws Exception {
		PageID tmp = new PageID (  ); 
		for ( int i = 0 ; i < runs ; i ++ ) {
			tmp.pageId = pageid.pageId + i ; 
			if ( getDiskMap ( tmp ) != 0 ) return i ;
		}
		return runs ;  
	}
	
	public void deallocatePage( PageID pid ) throws DBCannotDelPage, DBDiskMapException{
		deallocatePage ( pid, 1 ) ; 
	}
	
	public synchronized void deallocatePage ( PageID pid, int runs ) throws DBCannotDelPage, DBDiskMapException  {
		if ( pid.pageId <= mapPages || pid.pageId + runs > numPages ) throw
			new DBCannotDelPage ( null, "Can not del consecutive " + runs + " pages starting from " + pid.pageId ) ;
		PageID pageToDel = new PageID () ; 
		for ( int i = 0; i < runs ; i ++ ) { 
			pageToDel.pageId = pid.pageId + i ;
			setDiskMap ( pageToDel, 0 ) ; 
		}
		emptyCount = 0 ; 
	}
	
	public synchronized int readPage(PageID pid, Page aPage) throws DBNotInitException, DBInvalidPageIDException, DbIOException 
	{
		pageRead ++ ; 
		if ( fp == null ) throw new DBNotInitException ( null ) ; 
		if ( pid.pageId < 0 || pid.pageId > numPages ) 
			throw new DBInvalidPageIDException( null, pid ) ;
		
		try{
			fp.seek( (long) pid.pageId * GlobalConst.PAGESIZE ) ;
			fp.read( aPage.getData() , 0 , GlobalConst.PAGESIZE ) ;
		}catch ( IOException e ) {
			throw new DbIOException ( e, dbFileName ) ;
		}
		return 0;
	}
	
	public synchronized int writePage(PageID pid, Page aPage) throws
	DBNotInitException, DBInvalidPageIDException, DbIOException{
		pageWrote ++ ;
		if ( fp == null ) throw new DBNotInitException ( null ) ; 
		if ( pid.pageId < 0 || pid.pageId > numPages ) 
			throw new DBInvalidPageIDException ( null, pid ) ;
		
		try { 
			fp.seek( (long) pid.pageId * GlobalConst.PAGESIZE ) ;
			fp.write( aPage.getData() ) ;
		}catch ( IOException e ) {
			throw new DbIOException ( e, dbFileName ) ;
		}
		return 0 ; 
	}

	public ArrayList<String> findAllTemp() throws DBGetEntryException {
		PageID tmpPage = new PageID ( 0 ) ; 
		PageID currentPage = new PageID ( 0 ) 	;
		//dbname = dbname.toLowerCase() ;
		ArrayList < String > result = new ArrayList < String > () ;  
		String p="[?]*tmp_[?]*";
		Pattern pp=Pattern.compile(p);
		try {
			while ( currentPage.pageId != Page.INVALID_PAGE.pageId ) { 
				Page aPage = new Page ( false ) ;
				GoggleDB.bufferMgr.pinPage( currentPage , aPage, false );

				HeaderPage headerPage ;
				if ( currentPage.pageId == 0 ) headerPage = new FirstPage ( aPage );
				else headerPage = new HeaderPage ( aPage ) ;

				int i ; 
				for ( i = 0 ; i < headerPage.NUM_ENTRY ; i ++ ) {
					NameEntry entry = headerPage.getEntry( i ) ;
					if ( entry.firstPage.pageId == 0 ) continue ;
					else if ( pp.matcher(entry.getName()).find()) 
						result.add( entry.getName() ) ; 
				}
				
				tmpPage.pageId = currentPage.pageId ; 
				currentPage = headerPage.getNextPage() ; 
				GoggleDB.bufferMgr.unpinPage( tmpPage, false );				
			}
			return result ;
		}
		catch ( Exception e ) {
			throw new DBGetEntryException (e, "all" ) ; 
		}
	}

}
